﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;

internal sealed class RazorProjectPageRouteModelProvider : IPageRouteModelProvider
{
    private const string AreaRootDirectory = "/Areas";
    private readonly RazorProjectFileSystem _razorFileSystem;
    private readonly RazorPagesOptions _pagesOptions;
    private readonly PageRouteModelFactory _routeModelFactory;
    private readonly ILogger<RazorProjectPageRouteModelProvider> _logger;

    public RazorProjectPageRouteModelProvider(
        RazorProjectFileSystem razorFileSystem,
        IOptions<RazorPagesOptions> pagesOptionsAccessor,
        ILoggerFactory loggerFactory)
    {
        _razorFileSystem = razorFileSystem;
        _pagesOptions = pagesOptionsAccessor.Value;
        _logger = loggerFactory.CreateLogger<RazorProjectPageRouteModelProvider>();
        _routeModelFactory = new PageRouteModelFactory(_pagesOptions, _logger);
    }

    /// <remarks>
    /// Ordered to execute after <see cref="CompiledPageRouteModelProvider"/>.
    /// </remarks>
    public int Order => -1000 + 10;

    public void OnProvidersExecuted(PageRouteModelProviderContext context)
    {
    }

    public void OnProvidersExecuting(PageRouteModelProviderContext context)
    {
        // When RootDirectory and AreaRootDirectory overlap, e.g. RootDirectory = /, AreaRootDirectory = /Areas;
        // we need to ensure that the page is only route-able via the area route. By adding area routes first,
        // we'll ensure non area routes get skipped when it encounters an IsAlreadyRegistered check.

        AddAreaPageModels(context);
        AddPageModels(context);
    }

    private void AddPageModels(PageRouteModelProviderContext context)
    {
        foreach (var item in _razorFileSystem.EnumerateItems(_pagesOptions.RootDirectory))
        {
            var relativePath = item.CombinedPath;
            if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase)))
            {
                // A route for this file was already registered either by the CompiledPageRouteModel or as an area route.
                // by this provider. Skip registering an additional entry.

                // Note: We're comparing duplicates based on root-relative paths. This eliminates a page from being discovered
                // by overlapping area and non-area routes where ViewEnginePath would be different.
                continue;
            }

            if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
            {
                // .cshtml pages without @page are not RazorPages.
                continue;
            }

            var routeModel = _routeModelFactory.CreateRouteModel(relativePath, routeTemplate);
            if (routeModel != null)
            {
                context.RouteModels.Add(routeModel);
            }
        }
    }

    private void AddAreaPageModels(PageRouteModelProviderContext context)
    {
        foreach (var item in _razorFileSystem.EnumerateItems(AreaRootDirectory))
        {
            var relativePath = item.CombinedPath;
            if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase)))
            {
                // A route for this file was already registered either by the CompiledPageRouteModel.
                // Skip registering an additional entry.
                continue;
            }

            if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
            {
                // .cshtml pages without @page are not RazorPages.
                continue;
            }

            var routeModel = _routeModelFactory.CreateAreaRouteModel(relativePath, routeTemplate);
            if (routeModel != null)
            {
                context.RouteModels.Add(routeModel);
            }
        }
    }
}
